package anonymouschat.service;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.swing.JOptionPane;
import javax.swing.JPasswordField;
import javax.swing.JProgressBar;

import anonymouschat.client.PublicIdentity;
import anonymouschat.exceptions.ConnectionException;
import anonymouschat.security.KeySalt;
import anonymouschat.utils.LocalFileOps;
import anonymouschat.utils.Print;

/**
 * Main server object, sets the server name and port based on command line
 * arguments
 * */
public class Server extends Thread {

	/**
	 * Maps username or server names connected to this server to their outbound
	 * channel
	 */
	public static volatile ConcurrentMap<String, Connection> currentConnections = new ConcurrentHashMap<String, Connection>();

	/**
	 * Maps users to their public identity (public keys)
	 */
	public static volatile ConcurrentHashMap<String, PublicIdentity> userNameToIdMap = new ConcurrentHashMap<String, PublicIdentity>();

	/**
	 * A list of all the users published at this server
	 */
	public static volatile LinkedList<String> locallyPublishedUsers = new LinkedList<String>();

	/**
	 * Maps a username to the last time the server received a user login message
	 */
	public static volatile ConcurrentHashMap<String, Long> userNameToLastHeardFrom = new ConcurrentHashMap<String, Long>();

	/**
	 * &lt Username, HostingServer &gt Tuple Discover a user's publishing
	 * server, this server knows a way to get back to the user, but not the
	 * entire route
	 */
	public static volatile ConcurrentHashMap<String, String> usernameToHostingServer = new ConcurrentHashMap<String, String>();

	// the name of this server thread
	private String serverName = "";

	// Identity of the current server (public/private keys)
	public ServerIdentObject serverIdentity;

	// Port the server listens on (updated later)
	private int port = -1;

	/**
	 * Maps a pseudo-name (connection number) to another pseudo-name(connection
	 * number) & socket
	 */
	public static final ServerConnectionIdManager connectionIdManager = new ServerConnectionIdManager();

	// Whether the server is initialized and ready to accept messages (used for
	// GUI)
	private boolean isReady = false;

	// Whether someone has closed the server GUI, and the actual server should
	// shutdown
	private boolean shutDown = false;

	// Mapping from user id to their pseudo-name (connection number) at the next
	// hop in the route towards the recipient user (used only on publication
	// server)
	private static volatile ConcurrentHashMap<String, String> userToConnectionId = new ConcurrentHashMap<String, String>();

	// The socket that the server listens on for new connections, or new
	// messages
	private ServerSocket serverSocket;

	private ServerGUI myGUI;

	private static String alerts = "";

	private static String password = null;

	/**
	 * 
	 * @param name
	 *            the server name
	 * @param portno
	 *            the port to use for accepting connections
	 */
	public Server(String name, int portno) {
		serverName = name;
		port = portno;
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// Initial implementation uses arguments for server names
		if (args.length > 1) {
			Server s = new Server(args[0], Integer.parseInt(args[1]));
			Print.info("Starting server " + s.getName() + ". . .");
			s.start();
			if (args.length == 3) {
				password = args[2];
			}
		} else {
			Print.error("You need to provide a name and port for this server");
			System.exit(0);
		}
	}

	/**
	 * Whether the server is initialized and ready to receive messages
	 * 
	 * @return true if ready
	 */
	private boolean isReady() {
		return this.isReady;
	}

	/**
	 * Return the server name associated with the currently running server
	 * 
	 * @return String containing server name of this server
	 */
	public String getServerName() {
		return serverName;
	}

	/**
	 * Run server thread (open new socket and listen for announce messages)
	 */
	@Override
	public void run() {
		// Opens server identity and initializes socket for server to listen on
		initialize();

		// Wait until socket is open to accept messages / new connections
		while (!this.isReady()) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				Print.error("Error sleeping - you're hosed!", e);
			}
		}

		// Launch server GUI
		myGUI = new ServerGUI(this);
		Print.info("Starting server GUI. . . ");
		myGUI.start();

		// Until the server GUI is closed accept new messages and handle them
		Print.info("Listening for messages.");
		while (!shutDown) {
			try {
				listenForMessage();
			} catch (Exception e) {
				Print.error("cannot listen for message", e);
				continue;
			}
		}
		Print.debug("Exiting. . .");
	}

	/**
	 * Open server identity and create socket for listening
	 */
	public void initialize() {
		serverSocket = null;
		try {
			Print.debug("Trying to open server identity for " + serverName);
			// Prompt the admin of the server for a password used to encrypt
			// identity
			if (Server.password == null) {
				Server.password = this.getPassword();
			}

			// If we already have a server identity created for this server
			if (LocalFileOps.identityExists(serverName)) {
				serverIdentity = ServerIdentObject.loadPrivateIdentity(
						serverName, password);
			} else { // Create a new server identity, one does not already exist
				JProgressBar progress = this
						.showProgressPane("Creating new identity. . .");
				Print.debug("Creating new identity. . .");
				KeySalt newSalt = new KeySalt();
				serverIdentity = new ServerIdentObject(serverName, password,
						newSalt, this);
				progress.setString("Created Identity");
				progress.setIndeterminate(false);
				progress.setValue(100);
			}
			password = null;

			// Open server socket to listen for new connections
			serverSocket = new ServerSocket(port);
			Print.info(serverName + ": Listening on port " + port);
		} catch (UnknownHostException e) {
			Print.error("Cannot load identity - unknown host", e);
			System.exit(1);
		} catch (IOException e) {
			Print.error("Cannot load identity - I/O error", e);
			System.exit(1);
		} catch (Exception e) {
			Print.error("Cannot load identity", e);
			System.exit(1);
		}

		// done initializing, ready for messages:
		this.isReady = true;
	}

	/**
	 * Displays progress when creating or opening an identity
	 * 
	 * @param string
	 * 
	 * @return Progress bar that will display until the identity is
	 *         created/opened
	 */
	private JProgressBar showProgressPane(String string) {
		final JProgressBar prog = new JProgressBar();
		prog.setIndeterminate(true);
		prog.setString(string);
		prog.setStringPainted(true);
		Thread t = new Thread() {

			@Override
			public void run() {
				Object[] obj = { prog };
				JOptionPane.showMessageDialog(null, obj, "Progress",
						JOptionPane.PLAIN_MESSAGE);
			}
		};
		t.start();

		return prog;
	}

	/**
	 * Prompt the user for a password (GUI) and make sure it fits our password
	 * criteria
	 * 
	 * @return Password as string
	 */
	private String getPassword() {
		String password = null;
		password = this.promptForPassword();
		if (password == null || password.equalsIgnoreCase("")) {
			Print.error("Cannot use a blank password");
			System.exit(1);
		}
		return password;
	}

	/**
	 * Give user a popup asking to provide a password (GUI)
	 * 
	 * @return Password as a string
	 */
	private String promptForPassword() {
		JPasswordField passwordField = new JPasswordField();
		Object[] obj = { "Please input the password for " + serverName + ":",
				passwordField };
		Object stringArray[] = { "OK", "Cancel" };
		String password = null;
		if (JOptionPane.showOptionDialog(null, obj, "Password",
				JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
				stringArray, obj) == JOptionPane.YES_OPTION) {
			password = new String(passwordField.getPassword());
		}
		return password;
	}

	/**
	 * Wait for an announce message, when received store the connection and
	 * return
	 * 
	 * @throws Exception
	 */
	public void listenForMessage() throws Exception {

		// Wait for new connection
		Connection newConnection = null;
		try {
			newConnection = new Connection(this.serverName);
			newConnection.openCom(serverSocket.accept());
		} catch (IOException e) {
			Print.error("Accept failed.", e);
		} catch (ConnectionException e) {
			Print.error("Creating the in/out streams failed.", e);
		}

		// Thread used to handle messages coming into this server on the newly
		// created connection
		InConnectionThread newInConnection = new InConnectionThread(
				newConnection, this);
		newInConnection.start();

	}

	// Base number that connection numbers are created from
	private static Long connectionNumber = System.currentTimeMillis();

	// Returns a unique number for each user session
	public static Long generateConnectionId() {
		return connectionNumber++;
	}

	/**
	 * Return the port this server is running on
	 * 
	 * @return &ltint&gt Port this server is running on
	 * */
	public int getServerPort() {
		return port;
	}

	/**
	 * Returns the pseudo-name (connection number) associated with a username to
	 * route from a publication server to the user
	 * 
	 * @param destinationUser
	 * @return &lt Long &gt the connection number
	 */
	public static String getConnectionIdFor(String destinationUser) {
		return userToConnectionId.get(destinationUser);
	}

	/**
	 * Adds entry to map username to pseudo-name (connection number)
	 * 
	 * @note Downstream means towards a client, away from a publication server
	 * @param user
	 * @param downstreamId
	 */
	public static void addUserConnectionId(String user, String downstreamId) {
		userToConnectionId.put(user, downstreamId);
	}

	/**
	 * Get a list of the users currently logged onto the entire service
	 * 
	 * @return &lt String&gt a list of all the users online and where they are
	 *         published
	 */
	public String getOnlineUsers() {
		StringBuffer sb = new StringBuffer();
		for (String user : Server.usernameToHostingServer.keySet()) {
			sb.append(Server.getShortUserName(user) + "\t\t\t"
					+ Server.usernameToHostingServer.get(user) + "\n");
		}
		return sb.toString();
	}

	/**
	 * 
	 * @return &ltString&gt a list of all the users this server is publishing
	 */
	public String getPublishedUsers() {
		StringBuffer sb = new StringBuffer();
		for (String user : Server.locallyPublishedUsers) {
			sb.append(user + "\n");
		}
		return sb.toString();
	}

	/**
	 * 
	 * @param user
	 *            a long user ID string
	 * @return &ltString&gt "the first two clusters. . . the last two clusters"
	 */
	public static String getShortUserName(String user) {
		String ret = "";
		String tmp[] = null;
		tmp = user.split("-");
		if (user.startsWith("-")) {
			tmp[0] = "-" + tmp[1];
			tmp[1] = tmp[2];
		}
		ret += tmp[0] + "-" + tmp[1] + "-. . .-" + tmp[tmp.length - 2] + "-"
				+ tmp[tmp.length - 1];
		return ret;

	}

	/**
	 * 
	 * @return &ltString&gt the list of open connections, with their connection
	 *         numbers
	 */
	public static String getAllConnections() {
		return (Server.connectionIdManager.getConnectionMultiMap());
	}

	/**
	 * set the shutdown parameter to true to close the server
	 * @param shutDown
	 */
	public void setShutDown(boolean shutDown) {
		this.shutDown = shutDown;
	}

	/**
	 * Add an alert to the alerts String
	 * @param alert
	 */
	public static void addAlert(String alert) {
		if (alerts.length() > 0) {
			alerts += "\n";
		}
		alerts += alert + "\n";
	}

	/**
	 * Returns a string of all the new alerts this server is aware of
	 * @return
	 */
	public static String getNewAlerts() {
		if (alerts.length() == 0) {
			return null;
		}
		String temp = new String(alerts);
		alerts = "";
		return temp;
	}

}
